<!DOCTYPE HTML>
<html>
<head>
  <title>Test BiquadFilterNode All Pass Filter</title>
  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<pre id="test">
<script src="audio-testing.js"></script>
<script src="biquad-filters.js"></script>
<script src="biquad-testing.js"></script>
<script src="webaudio.js" type="text/javascript"></script>
<script class="testbody" type="text/javascript">

SimpleTest.waitForExplicitFinish();

addLoadEvent(function() {
// Test the frequency response of a biquad filter.  We compute the frequency response for a simple
// peaking biquad filter and compare it with the expected frequency response.  The actual filter
// used doesn't matter since we're testing getFrequencyResponse and not the actual filter output.
// The filters are extensively tested in other biquad tests.

var context;

// The biquad filter node.
var filter;

// The magnitude response of the biquad filter.
var magResponse;

// The phase response of the biquad filter.
var phaseResponse;

// Number of frequency samples to take.
var numberOfFrequencies = 1000;

// The filter parameters.
var filterCutoff = 1000; // Hz.
var filterQ = 1;
var filterGain = 5; // Decibels.

// The maximum allowed error in the magnitude response.
var maxAllowedMagError = 5.7e-7;

// The maximum allowed error in the phase response.
var maxAllowedPhaseError = 4.7e-8;

// The magnitudes and phases of the reference frequency response.
var magResponse;
var phaseResponse;

// The magnitudes and phases of the reference frequency response.
var expectedMagnitudes;
var expectedPhases;

// Convert frequency in Hz to a normalized frequency between 0 to 1 with 1 corresponding to the
// Nyquist frequency.
function normalizedFrequency(freqHz, sampleRate)
{
    var nyquist = sampleRate / 2;
    return freqHz / nyquist;
}

// Get the filter response at a (normalized) frequency |f| for the filter with coefficients |coef|.
function getResponseAt(coef, f)
{
    var b0 = coef.b0;
    var b1 = coef.b1;
    var b2 = coef.b2;
    var a1 = coef.a1;
    var a2 = coef.a2;

    // H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2)
    //
    // Compute H(exp(i * pi * f)).  No native complex numbers in javascript, so break H(exp(i * pi * // f))
    // in to the real and imaginary parts of the numerator and denominator.  Let omega = pi * f.
    // Then the numerator is
    //
    // b0 + b1 * cos(omega) + b2 * cos(2 * omega) - i * (b1 * sin(omega) + b2 * sin(2 * omega))
    //
    // and the denominator is
    //
    // 1 + a1 * cos(omega) + a2 * cos(2 * omega) - i * (a1 * sin(omega) + a2 * sin(2 * omega))
    //
    // Compute the magnitude and phase from the real and imaginary parts.

    var omega = Math.PI * f;
    var numeratorReal = b0 + b1 * Math.cos(omega) + b2 * Math.cos(2 * omega);
    var numeratorImag = -(b1 * Math.sin(omega) + b2 * Math.sin(2 * omega));
    var denominatorReal = 1 + a1 * Math.cos(omega) + a2 * Math.cos(2 * omega);
    var denominatorImag = -(a1 * Math.sin(omega) + a2 * Math.sin(2 * omega));

    var magnitude = Math.sqrt((numeratorReal * numeratorReal + numeratorImag * numeratorImag)
                              / (denominatorReal * denominatorReal + denominatorImag * denominatorImag));
    var phase = Math.atan2(numeratorImag, numeratorReal) - Math.atan2(denominatorImag, denominatorReal);

    if (phase >= Math.PI) {
        phase -= 2 * Math.PI;
    } else if (phase <= -Math.PI) {
        phase += 2 * Math.PI;
    }

    return {magnitude : magnitude, phase : phase};
}

// Compute the reference frequency response for the biquad filter |filter| at the frequency samples
// given by |frequencies|.
function frequencyResponseReference(filter, frequencies)
{
    var sampleRate = filter.context.sampleRate;
    var normalizedFreq = normalizedFrequency(filter.frequency.value, sampleRate);
    var filterCoefficients = createFilter(filter.type, normalizedFreq, filter.Q.value, filter.gain.value);

    var magnitudes = [];
    var phases = [];

    for (var k = 0; k < frequencies.length; ++k) {
        var response = getResponseAt(filterCoefficients, normalizedFrequency(frequencies[k], sampleRate));
        magnitudes.push(response.magnitude);
        phases.push(response.phase);
    }

    return {magnitudes : magnitudes, phases : phases};
}

// Compute a set of linearly spaced frequencies.
function createFrequencies(nFrequencies, sampleRate)
{
    var frequencies = new Float32Array(nFrequencies);
    var nyquist = sampleRate / 2;
    var freqDelta = nyquist / nFrequencies;

    for (var k = 0; k < nFrequencies; ++k) {
        frequencies[k] = k * freqDelta;
    }

    return frequencies;
}

function linearToDecibels(x)
{
    if (x) {
        return 20 * Math.log(x) / Math.LN10;
    } else {
        return -1000;
    }
}

// Look through the array and find any NaN or infinity. Returns the index of the first occurence or
// -1 if none.
function findBadNumber(signal)
{
    for (var k = 0; k < signal.length; ++k) {
        if (!isValidNumber(signal[k])) {
           return k;
        }
    }
    return -1;
}

// Compute absolute value of the difference between phase angles, taking into account the wrapping
// of phases.
function absolutePhaseDifference(x, y)
{
    var diff = Math.abs(x - y);

    if (diff > Math.PI) {
        diff = 2 * Math.PI - diff;
    }
    return diff;
}

// Compare the frequency response with our expected response.
function compareResponses(filter, frequencies, magResponse, phaseResponse)
{
    var expectedResponse = frequencyResponseReference(filter, frequencies);

    expectedMagnitudes = expectedResponse.magnitudes;
    expectedPhases = expectedResponse.phases;

    var n = magResponse.length;
    var success = true;
    var badResponse = false;

    var maxMagError = -1;
    var maxMagErrorIndex = -1;

    var k;
    var hasBadNumber;

    hasBadNumber = findBadNumber(magResponse);
    ok (hasBadNumber < 0, "Magnitude response has NaN or infinity at " + hasBadNumber);

    hasBadNumber = findBadNumber(phaseResponse);
    ok (hasBadNumber < 0, "Phase response has NaN or infinity at " + hasBadNumber);

    // These aren't testing the implementation itself.  Instead, these are sanity checks on the
    // reference.  Failure here does not imply an error in the implementation.
    hasBadNumber = findBadNumber(expectedMagnitudes);
    ok (hasBadNumber < 0, "Expected magnitude response has NaN or infinity at " + hasBadNumber);

    hasBadNumber = findBadNumber(expectedPhases);
    ok (hasBadNumber < 0, "Expected phase response has NaN or infinity at " + hasBadNumber);

    for (k = 0; k < n; ++k) {
        var error = Math.abs(linearToDecibels(magResponse[k]) - linearToDecibels(expectedMagnitudes[k]));
        if (error > maxMagError) {
            maxMagError = error;
            maxMagErrorIndex = k;
        }
    }

    var message = "Magnitude error (" + maxMagError + " dB)";
    message += " exceeded threshold at " + frequencies[maxMagErrorIndex];
    message += " Hz.  Actual: " + linearToDecibels(magResponse[maxMagErrorIndex]);
    message += " dB, expected: " + linearToDecibels(expectedMagnitudes[maxMagErrorIndex]) + " dB.";
    ok(maxMagError < maxAllowedMagError, message);

    var maxPhaseError = -1;
    var maxPhaseErrorIndex = -1;

    for (k = 0; k < n; ++k) {
        var error = absolutePhaseDifference(phaseResponse[k], expectedPhases[k]);
        if (error > maxPhaseError) {
            maxPhaseError = error;
            maxPhaseErrorIndex = k;
        }
    }

    message = "Phase error (radians) (" + maxPhaseError;
    message += ") exceeded threshold at " + frequencies[maxPhaseErrorIndex];
    message += " Hz.  Actual: " + phaseResponse[maxPhaseErrorIndex];
    message += " expected: " + expectedPhases[maxPhaseErrorIndex];

    ok(maxPhaseError < maxAllowedPhaseError, message);
}

context = new AudioContext();

filter = context.createBiquadFilter();

// Arbitrarily test a peaking filter, but any kind of filter can be tested.
filter.type = "peaking";
filter.frequency.value = filterCutoff;
filter.Q.value = filterQ;
filter.gain.value = filterGain;

var frequencies = createFrequencies(numberOfFrequencies, context.sampleRate);
magResponse = new Float32Array(numberOfFrequencies);
phaseResponse = new Float32Array(numberOfFrequencies);

filter.getFrequencyResponse(frequencies, magResponse, phaseResponse);
compareResponses(filter, frequencies, magResponse, phaseResponse);

SimpleTest.finish();
});
</script>
</pre>
</body>
</html>
